# -*- coding: utf-8 -*-
"""
UTc! module: server

Purpose
=======
 Provides a server-side entry point for the official UTc! release.
 
Legal
=====
 All code, unless otherwise indicated, is original, and subject to the
 terms of the GPLv2, which is provided in COPYING.
 
 (C) Neil Tallim, 2009
"""
import socket
import sys
import threading
import wx

import src.game as game
import src.lobby as lobby

EVT_GAMEOVER_ID = wx.NewId() #: An identifier for the custom GameOver event.
def EVT_GAMEOVER(window, function):
	"""
	Allows a custom GameOver event to be managed by the wx queue.
	
	@type window: wx.Window
	@param window: The object that will manage this event.
	@type function: callable
	@param function: The function that will handle this event.
	"""
	window.Connect(-1, -1, EVT_GAMEOVER_ID, function)
class GameOverEvent(wx.PyEvent):
	"""
	A custom GameOver event object.
	"""
	def __init__(self):
		"""
		Initialises this event.
		"""
		wx.PyEvent.__init__(self)
		self.SetEventType(EVT_GAMEOVER_ID)
		
class ServerGUI(wx.Frame):
	"""
	A GUI that manages the UTc! lobby and up to one game.
	"""
	_game = None #: The game that is currently in progress, if any.
	_lobby = None #: The server's lobby.
	_lock = None #: A lock used to prevent collisions between game-space, lobby-space, and user-space.
	
	def __init__(self, *args, **kwds):
		"""
		wxPython control initialisation routine.
		
		Also sets up event bindings and variables required by this GUI.
		"""
		kwds["style"] = wx.SYSTEM_MENU|wx.MINIMIZE_BOX|wx.CLOSE_BOX|wx.CAPTION
		wx.Frame.__init__(self, *args, **kwds)
		self.szv_settings_staticbox = wx.StaticBox(self, -1, "Settings")
		self.szv_environment_staticbox = wx.StaticBox(self, -1, "Environment")
		self.szv_obstacles_staticbox = wx.StaticBox(self, -1, "Obstacles")
		self.lbl_duration = wx.StaticText(self, -1, "Game length:")
		self.sld_duration = wx.Slider(self, -1, 10, 5, 60, style=wx.SL_HORIZONTAL|wx.SL_LABELS)
		self.lbl_duration_units = wx.StaticText(self, -1, "minutes")
		self.lbl_score = wx.StaticText(self, -1, "Score target:")
		self.sld_score = wx.Slider(self, -1, 20, 0, 200, style=wx.SL_HORIZONTAL|wx.SL_LABELS)
		self.lbl_score_units = wx.StaticText(self, -1, "points")
		self.lbl_port = wx.StaticText(self, -1, "Port: ")
		self.txt_port = wx.TextCtrl(self, -1, "64753", style=wx.ALIGN_RIGHT)
		self.lbl_respawn = wx.StaticText(self, -1, "Respawn delay:")
		self.sld_respawn = wx.Slider(self, -1, 5, 1, 30, style=wx.SL_HORIZONTAL|wx.SL_LABELS)
		self.lbl_respawn_units = wx.StaticText(self, -1, "seconds")
		self.lbl_shields = wx.StaticText(self, -1, "Shielding recovery:")
		self.sld_shields = wx.Slider(self, -1, 10, 5, 100, style=wx.SL_HORIZONTAL|wx.SL_LABELS)
		self.lbl_shields_units = wx.StaticText(self, -1, "%")
		self.lbl_average = wx.StaticText(self, -1, "Average size:")
		self.sld_average = wx.Slider(self, -1, 50, 10, 100, style=wx.SL_HORIZONTAL|wx.SL_LABELS)
		self.lbl_average_units = wx.StaticText(self, -1, "metres")
		self.lbl_variance = wx.StaticText(self, -1, "Size variance:")
		self.sld_variance = wx.Slider(self, -1, 25, 0, 50, style=wx.SL_HORIZONTAL|wx.SL_LABELS)
		self.lbl_variance_units = wx.StaticText(self, -1, "%")
		self.lbl_ratio = wx.StaticText(self, -1, "Sphere/cube ratio:")
		self.sld_ratio = wx.Slider(self, -1, 50, 0, 100, style=wx.SL_HORIZONTAL|wx.SL_LABELS)
		self.lbl_ratio_units = wx.StaticText(self, -1, "%")
		self.lbl_radius = wx.StaticText(self, -1, "Radius:")
		self.sld_radius = wx.Slider(self, -1, 3, 1, 25, style=wx.SL_HORIZONTAL|wx.SL_LABELS)
		self.lbl_radius_units = wx.StaticText(self, -1, "layers")
		self.lbl_dispersion = wx.StaticText(self, -1, "Dispersion:")
		self.sld_dispersion = wx.Slider(self, -1, 250, 100, 1000, style=wx.SL_HORIZONTAL|wx.SL_LABELS)
		self.lbl_dispersion_units = wx.StaticText(self, -1, "metres")
		self.btn_launch = wx.Button(self, -1, "Start a game")
		self.whitespace = wx.Panel(self, -1)
		
		self.__set_properties()
		self.__do_layout()
		
		#Set up event bindings.
		self.Bind(wx.EVT_CLOSE, self._quit)
		self.btn_launch.Bind(wx.EVT_BUTTON, self._clickLaunchButton)
		EVT_GAMEOVER(self, self._gameOver)
		
		#Set up the game-/lobby-/user-space lock.
		self._lock = threading.Lock()
		
		#Attempt to start the UTc! lobby.
		#If the user provided a prot number as an argument, run on that port;
		#if not, run on the default port.
		port = 64775
		try:
			if len(sys.argv) > 1:
				port = int(sys.argv[1])
			self._lobby = lobby.Lobby(port, self._addPlayer, self._validatePlayer)
			self._lobby.start()
		except socket.error:
			wx.MessageDialog(self, "Unable to start server on port %i." % (port), "Unable to start server", wx.OK).ShowModal()
			self.Destroy()
		except:
			wx.MessageDialog(self, "'%s' is not a valid port number." % (sys.argv[1]), "Unable to start server", wx.OK).ShowModal()
			self.Destroy()
			
	def gameOver(self):
		"""
		Terminates the in-progress game, if any.
		
		This function may be passed to a game thread as a termination callback.
		"""
		try:
			self._lock.acquire()
			if self._game:
				self._game.kill()
				self._lobby.endGame(self._game.getTeams(), self._game.getPlayers())
				self._game = None
				wx.PostEvent(self, GameOverEvent())
			self._lock.release()
		except wx.PyDeadObjectError: #The GUI was deleted before the game server, somehow.
			pass
			
	def _addPlayer(self, player_name, team_id, colour, ship, bolts, bolts_modifiers, missiles, power_source):
		"""
		Attempts to authorize a player to join the game, if one is in progress.
		
		@type player_name: basestring
		@param player_name: The name of the player to authorize.
		@type team_id: int
		@param team_id: The ID of the team the player is to join.
		@type colour: tuple(3)
		@param colour: The (r, g, b) values of the player's ship colour.
		@type ship: str
		@param ship: The ID of the player's ship class.
		@type bolts: sequence(4)
		@param bolts: A collection of bolt IDs or null-entries.
		@type bolts_modifiers: sequence(4)
		@param bolts_modifier: A collection of bolt modifier values.
		@type missile: sequence(4)
		@param missiles: A collection of missile IDs or null-entries.
		@type power_source: int
		@param power_source: The player's ship's power source.
		
		@rtype: str|None
		@return: None if authorization succeeded or a string describing the
		    problem otherwise.
		"""
		self._lock.acquire()
		try:
			if self._game:
				return self._game.addPlayer(
				 player_name, team_id,
				 colour, ship,
				 [bolt for bolt in bolts if not bolt == '__'],
				 [modifier for modifier in bolts_modifiers if modifier],
				 [missile for missile in missiles if not missile == '__'],
				 power_source
				)
			return "no game in progress"
		finally:
			self._lock.release()
			
	def _clickLaunchButton(self, event):
		"""
		Attempts to start a game or prompts to abort a game in progress.
		
		@type event: wx.PyEvent
		@param event: The event that triggered this function.
		"""
		self._lock.acquire()
		if not self._game:
			if not self.txt_port.GetValue().isdigit():
				self._lock.release()
				wx.MessageDialog(self, "Ports must be numeric", "Port value error", wx.OK).ShowModal()
			else:
				try:
					game_port = int(self.txt_port.GetValue())
					self._game = game.Game(
					 game_port,
					 self.sld_duration.GetValue(),
					 self.sld_score.GetValue(),
					 self.gameOver,
					 float(self.sld_respawn.GetValue()),
					 self.sld_shields.GetValue() / 100.0,
					 self.sld_average.GetValue(),
					 self.sld_variance.GetValue() / 100.0,
					 self.sld_ratio.GetValue() / 100.0,
					 self.sld_radius.GetValue(),
					 self.sld_dispersion.GetValue()
					)
					self._game.start()
					self._lobby.startGame(game_port)
					self.btn_launch.SetLabel("Abort current game")
					self._lock.release()
				except socket.error:
					self._lock.release()
					wx.MessageDialog(self, "Unable to bind to specified port", "Port value error", wx.OK).ShowModal()
		else:
			self._lock.release()
			if wx.MessageDialog(self, "Do you want to interrupt the game?", "Abort game?", wx.YES_NO | wx.NO_DEFAULT).ShowModal() == wx.ID_YES:
				self.gameOver()
				
	def _gameOver(self, event):
		"""
		Processes a GameOver event, resetting the GUI after a game has ended.
		
		@type event: wx.PyEvent
		@param event: The event that triggered this function.
		"""
		self.btn_launch.SetLabel("Start a game")
		
	def _quit(self, event):
		"""
		Attempts to end this application; presents a warning message if a game is
		currently in progress.
		
		@type event: wx.PyEvent
		@param event: The event that triggered this function.
		"""
		show_warning = False
		self._lock.acquire()
		if not self._game:
			self._lobby.kill()
			self.Destroy()
		else:
			show_warning = True
		self._lock.release()
		if show_warning:
			wx.MessageDialog(self, "A game is currently in progress", "Unable to quit", wx.OK).ShowModal()
			
	def _validatePlayer(self, player_name):
		"""
		Determines whether the named player is currently in the ongoing game, if
		any.
		
		For the sake of players looking to join an in-progress game, a return of
		False is desirable.
		
		@type player_name: basestring
		@param player_name: The name of the player seeking validation.
		
		@rtype: bool
		@return: False if the player is not currently in the ongoing game, if any.
		"""
		self._lock.acquire()
		try:
			return self._game and self._game.hasPlayer(player_name)
		finally:
			self._lock.release()
			
	def __set_properties(self):
		"""
		wxPython control configuration routine.
		"""
		self.SetTitle("Ultra Tank-chan! Server")
		self.SetSize((300, 480))
		
	def __do_layout(self):
		"""
		wxPython control layout routine.
		"""
		szh_main = wx.BoxSizer(wx.HORIZONTAL)
		szv_config = wx.BoxSizer(wx.VERTICAL)
		szv_settings = wx.StaticBoxSizer(self.szv_settings_staticbox, wx.VERTICAL)
		szv_environment = wx.StaticBoxSizer(self.szv_environment_staticbox, wx.VERTICAL)
		szv_obstacles = wx.StaticBoxSizer(self.szv_obstacles_staticbox, wx.VERTICAL)
		szh_dispersion = wx.BoxSizer(wx.HORIZONTAL)
		szh_radius = wx.BoxSizer(wx.HORIZONTAL)
		szh_ratio = wx.BoxSizer(wx.HORIZONTAL)
		szh_variance = wx.BoxSizer(wx.HORIZONTAL)
		szh_average = wx.BoxSizer(wx.HORIZONTAL)
		szh_shields = wx.BoxSizer(wx.HORIZONTAL)
		szh_respawn = wx.BoxSizer(wx.HORIZONTAL)
		szh_port = wx.BoxSizer(wx.HORIZONTAL)
		szh_score = wx.BoxSizer(wx.HORIZONTAL)
		szh_duration = wx.BoxSizer(wx.HORIZONTAL)
		szh_port.Add(self.lbl_port, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szh_port.Add(self.txt_port, 0, 0, 0)
		szv_settings.Add(szh_port, 0, wx.EXPAND, 0)
		szh_duration.Add(self.lbl_duration, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szh_duration.Add(self.sld_duration, 1, wx.EXPAND, 0)
		szh_duration.Add(self.lbl_duration_units, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szv_settings.Add(szh_duration, 1, wx.EXPAND, 0)
		szh_score.Add(self.lbl_score, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szh_score.Add(self.sld_score, 1, wx.EXPAND, 0)
		szh_score.Add(self.lbl_score_units, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szv_settings.Add(szh_score, 1, wx.EXPAND, 0)
		szh_respawn.Add(self.lbl_respawn, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szh_respawn.Add(self.sld_respawn, 1, wx.EXPAND, 0)
		szh_respawn.Add(self.lbl_respawn_units, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szv_environment.Add(szh_respawn, 1, wx.EXPAND, 0)
		szh_shields.Add(self.lbl_shields, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szh_shields.Add(self.sld_shields, 1, wx.EXPAND, 0)
		szh_shields.Add(self.lbl_shields_units, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szv_environment.Add(szh_shields, 1, wx.EXPAND, 0)
		szh_average.Add(self.lbl_average, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szh_average.Add(self.sld_average, 1, wx.EXPAND, 0)
		szh_average.Add(self.lbl_average_units, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szv_obstacles.Add(szh_average, 1, wx.EXPAND, 0)
		szh_variance.Add(self.lbl_variance, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szh_variance.Add(self.sld_variance, 1, wx.EXPAND, 0)
		szh_variance.Add(self.lbl_variance_units, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szv_obstacles.Add(szh_variance, 1, wx.EXPAND, 0)
		szh_ratio.Add(self.lbl_ratio, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szh_ratio.Add(self.sld_ratio, 1, wx.EXPAND, 0)
		szh_ratio.Add(self.lbl_ratio_units, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szv_obstacles.Add(szh_ratio, 1, wx.EXPAND, 0)
		szh_radius.Add(self.lbl_radius, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szh_radius.Add(self.sld_radius, 1, wx.EXPAND, 0)
		szh_radius.Add(self.lbl_radius_units, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szv_obstacles.Add(szh_radius, 1, wx.EXPAND, 0)
		szh_dispersion.Add(self.lbl_dispersion, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szh_dispersion.Add(self.sld_dispersion, 1, wx.EXPAND, 0)
		szh_dispersion.Add(self.lbl_dispersion_units, 0, wx.ALIGN_CENTER_VERTICAL, 0)
		szv_obstacles.Add(szh_dispersion, 1, wx.EXPAND, 0)
		szv_settings.Add(szv_environment, 0, wx.EXPAND, 0)
		szv_settings.Add(szv_obstacles, 0, wx.EXPAND, 0)
		szv_config.Add(szv_settings, 0, wx.EXPAND, 0)
		szv_config.Add(self.btn_launch, 0, wx.EXPAND, 0)
		szv_config.Add(self.whitespace, 1, wx.EXPAND, 0)
		szh_main.Add(szv_config, 2, 0, 0)
		self.SetSizer(szh_main)
		self.Layout()
		
if __name__ == "__main__":
	app = wx.PySimpleApp(0)
	wx.InitAllImageHandlers()
	wnd_main = ServerGUI(None, -1, "")
	app.SetTopWindow(wnd_main)
	wnd_main.Show()
	app.MainLoop()
	
	print "Please wait while networking threads terminate..."
	